unit frmEditor;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, LResources, Forms, ExtCtrls, SynHighlighterTeX,
  SynEdit, Graphics, SynEditKeyCmds, LCLType, Dialogs, StdCtrls, Buttons;


const UntitledDisplayString: String = '(untitled)';
type TChangeCaptionEvent = procedure(const ANewCaption: String) of object;

type

  { TEditorFrame }

  TEditorFrame = class(TFrame)
    autoBackupTimer: TIdleTimer;
    BitBtn1: TBitBtn;
    lbLog: TListBox;
    LogPanel: TPanel;
    Panel2: TPanel;
    seEditor: TSynEdit;
    Splitter1: TSplitter;
    procedure autoBackupTimerTimer(Sender: TObject);
    procedure BitBtn1Click(Sender: TObject);
    procedure seEditorChange(Sender: TObject);
    procedure seEditorCommandProcessed(Sender: TObject;
      var Command: TSynEditorCommand; var AChar: TUTF8Char; Data: pointer);
    procedure seEditorReplaceText(Sender: TObject; const ASearch,
      AReplace: string; Line, Column: integer;
      var ReplaceAction: TSynReplaceAction);
    procedure seEditorSpecialLineColors(Sender: TObject; Line: integer;
      var Special: boolean; var FG, BG: TColor);
  private
    { private declarations }
    // Changes to file
    FWasModified: Boolean;
    function GetChanged: Boolean;
    // Caption
    FOnChangeCaption: TChangeCaptionEvent;
    function GetCaption: String;
    function GetLanguage: String;
    procedure UpdateCaption;
    procedure SetOnChangeCaption(const AValue: TChangeCaptionEvent);
    // Filename
    FFilename: String;
    function BackupFilename: String;
    procedure SetFilename(const AValue: String);
    // Bracket mismatches
    FBracketMismatchLines: TIntegerSet;
    procedure ListMismatchingBrackets;
  public
    { public declarations }
    constructor Create(TheOwner: TComponent); override;
    // File commands
    procedure OpenFile(const AFilename: String);
    function SaveFile: Boolean;
    procedure GotFocus;
    // Edit commands
    procedure SelectAll;
    procedure Undo;                          function  CanUndo: Boolean;
    procedure Redo;                          function  CanRedo: Boolean;
    procedure Copy;                          function  CanCopy: Boolean;
    procedure Cut;                           function  CanCut: Boolean;
    procedure Paste;                         function  CanPaste: Boolean;
    procedure AutoJustifyParagraph;
    procedure FindMatchingBracket;
    // Logging
    procedure ClearLog;
    procedure Log(const AString: String);
    procedure FinishLog;
    // public properties
    property Language: String read GetLanguage;
    property Changed: Boolean read GetChanged;
    property Filename: String read FFilename write SetFilename;
    function DisplayFilename(const AUntitledString: String): String; overload;
    function DisplayFilename(): String; overload;
    property OnChangeCaption: TChangeCaptionEvent read FOnChangeCaption write SetOnChangeCaption;
  end;

implementation

uses untFunctions,
     dmMain,
     Controls; // for mrYes,mrNo,...

{ TEditorFrame }

procedure TEditorFrame.seEditorChange(Sender: TObject);
begin
  if (FWasModified = seEditor.Modified) then Exit;
  FWasModified := seEditor.Modified;
  UpdateCaption;
  seEditor.SetDebugMarks(1, 3);
end;

procedure TEditorFrame.seEditorCommandProcessed(Sender: TObject;
  var Command: TSynEditorCommand; var AChar: TUTF8Char; Data: pointer);
begin
  if Command = ecMatchBracket then
    ListMismatchingBrackets;
  // When a bracket, backspace or delete was typed, also check for brackets
  if (Command = ecChar) and (Utf8ToAnsi(AChar)[1] in ['(', ')', '[', ']', '{', '}']) then
    ListMismatchingBrackets;
end;

procedure TEditorFrame.seEditorReplaceText(Sender: TObject; const ASearch,
  AReplace: string; Line, Column: integer; var ReplaceAction: TSynReplaceAction
  );
var MsgResult : Integer;
begin
  MsgResult := MessageDlg('Confirm replacement',
                      'Occurrence of "' + ASearch + '" found on line ' + IntToStr(Line) + '. Replace by "' + AReplace + '"?',
                      mtConfirmation,
                      [mbYes, mbNo, mbYesToAll, mbCancel],
                      0
                      );
  case MsgResult of
    mrYes: ReplaceAction := raReplace;
    mrNo: ReplaceAction := raSkip;
    mrYesToAll: ReplaceAction := raReplaceAll;
    mrCancel: ReplaceAction := raCancel;
  end;
end;

procedure TEditorFrame.seEditorSpecialLineColors(Sender: TObject;
  Line: integer; var Special: boolean; var FG, BG: TColor);
begin
  if Line - 1 in FBracketMismatchLines then
  begin
    BG := clRed;
    Special := True;
  end
  else
    Special := False;
end;

constructor TEditorFrame.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  seEditor.Highlighter := dataMain.HighlighterTeX;
end;

procedure TEditorFrame.autoBackupTimerTimer(Sender: TObject);
begin
  seEditor.Lines.SaveToFile(BackupFilename);
end;

procedure TEditorFrame.BitBtn1Click(Sender: TObject);
begin
  seEditor.SetFocus;
  LogPanel.Visible := False;
end;

function TEditorFrame.GetCaption: String;
begin
  Result := DisplayFilename();
  if Changed then
    Result := '*' + Result;
end;

function TEditorFrame.GetLanguage: String;
begin
  Result := 'Plaintext';
  if Assigned(seEditor.Highlighter) then
    Result := seEditor.Highlighter.LanguageName;
end;

procedure TEditorFrame.UpdateCaption;
begin
  if Assigned(OnChangeCaption) then
    OnChangeCaption(GetCaption());
end;

function TEditorFrame.GetChanged: Boolean;
begin
  Result := seEditor.Modified;
end;

function TEditorFrame.BackupFilename: String;
begin
  if Filename = '' then
     Result := '~untitled_file.tex'
  else
    Result := ExtractFilePath(Filename) + '~' + ExtractFileName(Filename);
end;

procedure TEditorFrame.SetFilename(const AValue: String);
var oldBackupFile: String;
begin
  oldBackupFile := BackupFilename;
  FFilename := AValue;
  if FileExists(oldBackupFile) then
    RenameFile(oldBackupFile, BackupFilename);
  UpdateCaption;
  seEditor.Highlighter := dataMain.FindHighlighterByExtension(ExtractFileExt(FFilename));
  if not Assigned(seEditor.Highlighter) then seEditor.Highlighter := dataMain.HighlighterTeX;
end;

procedure TEditorFrame.ListMismatchingBrackets;
var BracketList: String;
    i, j: Integer;
    LastBracket, TheChar: Char;
  function Closes(const AClosing, AOpening: Char): Boolean;
  begin
    Result := False;
    if (AOpening = '(') and (AClosing = ')') then Result := True;
    if (AOpening = '[') and (AClosing = ']') then Result := True;
    if (AOpening = '{') and (AClosing = '}') then Result := True;
  end;
begin
  FBracketMismatchLines := [];
  BracketList := '';
  Text := seEditor.Text;
  // Scan each line and each character of the line
  for i := 0 to seEditor.Lines.Count - 1 do
    for j := 1 to Length(seEditor.Lines[i]) do
    begin
      TheChar := (seEditor.Lines[i])[j];
      // If TheChar is opening bracket, save it in LastBracket and add to the list
      if TheChar in ['(', '[', '{'] then
      begin
        LastBracket := TheChar;
        BracketList := BracketList + LastBracket;
      end;
      // If it is a closing bracket, check for errors:
      if TheChar in [')', ']', '}'] then
      begin
        // If it closes the last opened, remove that from the list
        if Closes(TheChar, LastBracket) then
        begin
          Delete(BracketList, Length(BracketList), 1);
          if BracketList = '' then
             LastBracket := #0
          else
            LastBracket := BracketList[Length(BracketList)];
        end
        else
          // Otherwise, flag this line as containing an error
          Include(FBracketMismatchLines, i);
        // If the bracket does close the penultimate - e.g.: (...[...) - close that one
        if (Length(BracketList) >= 2) and Closes(TheChar, BracketList[Length(BracketList) - 1]) then
          Delete(BracketList, Length(BracketList) - 1, 1);
      end;
    end { continue j and i loop };
    seEditor.Invalidate;
end;

procedure TEditorFrame.SetOnChangeCaption(const AValue: TChangeCaptionEvent);
begin
  FOnChangeCaption := AValue;
  UpdateCaption;
end;

procedure TEditorFrame.OpenFile(const AFilename: String);
begin
  FFilename := AFilename;
  seEditor.Lines.LoadFromFile(AFilename);
  UpdateCaption;
  seEditor.Highlighter := dataMain.FindHighlighterByExtension(ExtractFileExt(FFilename));
  if not Assigned(seEditor.Highlighter) then seEditor.Highlighter := dataMain.HighlighterTeX;
end;

function TEditorFrame.SaveFile: Boolean;
begin
  Result := True;
  if FFilename = '' then Exit;
  try
    seEditor.Lines.SaveToFile(FFilename);
    seEditor.Modified := False;
    if FileExists(BackupFilename) then DeleteFile(BackupFilename);
  except
    Result := False;
  end;
  UpdateCaption;
end;

procedure TEditorFrame.GotFocus;
begin
  seEditor.SetFocus;
end;

procedure TEditorFrame.SelectAll;
begin
  seEditor.SelectAll;
end;

procedure TEditorFrame.Undo;
begin
  seEditor.Undo;
end;

function TEditorFrame.CanUndo: Boolean;
begin
  Result := seEditor.CanUndo;
end;

procedure TEditorFrame.Redo;
begin
  seEditor.Redo;
end;

function TEditorFrame.CanRedo: Boolean;
begin
  Result := seEditor.CanRedo;
end;

procedure TEditorFrame.Copy;
begin
  seEditor.CopyToClipboard;
end;

function TEditorFrame.CanCopy: Boolean;
begin
 Result :=  seEditor.SelAvail;
end;

procedure TEditorFrame.Cut;
begin
  seEditor.CutToClipboard;
end;

function TEditorFrame.CanCut: Boolean;
begin
 Result :=  seEditor.SelAvail;
end;

procedure TEditorFrame.Paste;
begin
  seEditor.PasteFromClipboard;
end;

function TEditorFrame.CanPaste: Boolean;
begin
 Result :=  seEditor.CanPaste;
end;

procedure TEditorFrame.AutoJustifyParagraph;

var SelStart, SelEnd: Integer;
begin
  SelStart := seEditor.SelStart;
  SelEnd := seEditor.SelEnd;
  seEditor.SelectParagraph;
  seEditor.SelText := AutoJustifyText(seEditor.SelText);
  seEditor.SelStart := SelStart;
  seEditor.SelEnd := SelEnd;
end;

procedure TEditorFrame.FindMatchingBracket;
begin
  seEditor.FindMatchingBracket;
end;

procedure TEditorFrame.ClearLog;
begin
  lbLog.Clear;
  LogPanel.Visible := True;
end;

procedure TEditorFrame.Log(const AString: String);
begin
  lbLog.ItemIndex := lbLog.Items.Add(AString);
  Application.ProcessMessages;
end;

procedure TEditorFrame.FinishLog;
begin
  Log('Log finished.');
  lbLog.ItemIndex := -1;
  seEditor.SetFocus;
end;

function TEditorFrame.DisplayFilename(const AUntitledString: String): String;
begin
  if (FFilename = '') then
    Result := AUntitledString
  else
    Result := ExtractFileName(FFilename);
end;

function TEditorFrame.DisplayFilename(): String;
begin
  Result := DisplayFilename(UntitledDisplayString);
end;

initialization
  {$I frmEditor.lrs}

end.

